/*
 *  Copyright 2011 Iurii Dunko, Iana Potochniak.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */

package org.opu.yyminijava.visitor;

import static org.opu.yyminijava.visitor.TypeCheckException.assertEquals;
import static org.opu.yyminijava.visitor.TypeCheckException.assertNotNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.opu.yyminijava.symboltable.Record;
import org.opu.yyminijava.symboltable.SymbolTable;
import org.opu.yyminijava.symboltable.VarRecord;
import org.opu.yyminijava.syntaxtree.Block;
import org.opu.yyminijava.syntaxtree.DoWhileStatement;
import org.opu.yyminijava.syntaxtree.ElseStatementPart;
import org.opu.yyminijava.syntaxtree.Expression;
import org.opu.yyminijava.syntaxtree.ForInit;
import org.opu.yyminijava.syntaxtree.ForStatement;
import org.opu.yyminijava.syntaxtree.ForUpdate;
import org.opu.yyminijava.syntaxtree.IfElseStatement;
import org.opu.yyminijava.syntaxtree.IfStatementPart;
import org.opu.yyminijava.syntaxtree.MethodCallStatement;
import org.opu.yyminijava.syntaxtree.MethodDeclaration;
import org.opu.yyminijava.syntaxtree.PostfixExpression;
import org.opu.yyminijava.syntaxtree.PrimitiveType;
import org.opu.yyminijava.syntaxtree.ReturnStatement;
import org.opu.yyminijava.syntaxtree.Rule;
import org.opu.yyminijava.syntaxtree.Statement;
import org.opu.yyminijava.syntaxtree.StatementExpression;
import org.opu.yyminijava.syntaxtree.Type;
import org.opu.yyminijava.syntaxtree.VariableDeclarator;
import org.opu.yyminijava.syntaxtree.WhileStatement;
import org.opu.yyminijava.syntaxtree.primary.PrimaryFromIdentifier;

import com.sun.org.apache.xalan.internal.xsltc.compiler.CompilerException;

/**
 * <ol>What do this handler do ?
 *   <li>Check undeclared identifiers </li>
 *   <li>Expressions analysis - for example:
 *      <pre>
 *  AddExp → MultExp (AddOp MultExp) ∗
 *      </pre>
 *      and it containe second part, we must verifi thet both
 *      part is the same types, or one of it is string
 *   </li>
 *   <li>Statements thet look like:
 *      <pre>
 *  leftPart = rightPart;
 *      </pre>
 *      verifi thet both part is the same types.
 *   </li>
 *   <li>Methods call :<ol>
 *       <li>Check that calling method is exist in class</li>
 *       <li>Check that type of return expression equals declared return type</li>
 *       <li>Check the number of parameters and their types</li>
 *     </ol>
 *    </li>
 *   <li>If it array it can have only one attribute - length (implemented in 2)</li>
 *   <li>if(bolean); while(boolean)</li>
 *   <li>verify that variables have a value before they are used (not implemented)</li>
 * </ol>
 *
 * <ol>According to this list we can define list of rules that we will check:
 *   <li>{@link StatementExpression}
 *     <pre>ID = {@link Expression}
 *     </pre>
 *     (This is a part of {@link ForInit} statement).
 *     Here we got to check that symbol table contains record about identifier;
 *     Check tath both part of statement of the same type;
 *     Check expression;
 *   </li>
 *   <li>{@link ForStatement}
 *     <pre>for({@link ForInit};{@link Expression}; {@link ForUpdate})
 *     </pre>
 *     Here we got to check tat expression is boolean type (if it exist);
 *     Check expression;
 *   </li>
 *   <li>{@link ReturnStatement}
 *     <pre>return {@link Expression};
 *     </pre>
 *     Here we got to check if the declared method really must to return type
 *     of expression;
 *     Check expression;
 *   </li>
 *   <li>{@link WhileStatement} and {@link DoWhileStatement}
 *     <pre>while({@link Expression}){...}
 *     </pre>
 *     <pre>do{...}while({@link Expression});
 *     </pre>
 *     Here we got to check Expression and it type must be boolean.
 *   </li>
 *   <li>{@link IfStatementPart}
 *     <pre>if ({@link Expression})
 *     </pre>
 *     Here we got to check Expression and it type must be boolean (like in
 *     while statement).
 *   </li>
 *   <li>{@link MethodCallStatement}
 *     <pre>{@link PostfixExpression} = {@link Expression}
 *     </pre>
 *     Here we got to check PostfixExpression;
 *     It the second part is exist we also got to check it and check that both
 *     part have the same type.
 *   </li>
 *   <li>{@link VariableDeclarator}
 *     <pre>ID = {@link Expression}
 *     </pre>
 *     Here we got to check that symbol table contains record about identifier;
 *     Check tath both part of statement of the same type;
 *     Check expression;
 *     (like in StatementExpression statement)
 *   </li>
 *   <li>{@link PrimaryFromIdentifier}
 *     Here we got to check that symbol table contains record about identifier;
 *   </li>
 * </ol>
 * @author Iurii Dunko
 * @author Iana Potochniak
 */
public class TypeCheckHandler implements VisitorHandler {

    private SymbolTable symbolTable;
    private ArrayList<Exception> problems = new ArrayList<Exception>();
    private static final PrimitiveType BOOL = PrimitiveType.BOOL;

    public TypeCheckHandler(SymbolTable symbolTable) {
        this.symbolTable = symbolTable;
    }

    @Override
    public void onVisitStart(Rule rule, boolean isAbstractRule) {
        TypeCheckingHelper helper = new TypeCheckingHelper(symbolTable, "!");

        try {
            if (rule instanceof StatementExpression){
               StatementExpression stEx = (StatementExpression) rule;
               checkVarInit(stEx.getName(), stEx.getExpression(), helper);
            }

            else if (rule instanceof ForStatement){
                ForStatement fSt = (ForStatement) rule;
                Expression fExp = fSt.getForExpression();
                if (fExp != null) {
					checkBoolExp(fExp, "for", helper);
				}
            }

            else if (rule instanceof MethodDeclaration) {
                // We check return statement in this level

                MethodDeclaration md = (MethodDeclaration) rule;
                Type type = md.getHeader().getType();

                List<ReturnStatement> list = new ArrayList<ReturnStatement>();
                lookForReturnStatement(list, md.getBody().getBlock());

                for (ReturnStatement rs : list) {
                    compare(type, rs.getExpression(), helper);
                }
            }

            else if ((rule instanceof WhileStatement)
                    || (rule instanceof DoWhileStatement)){

                WhileStatement whlSt = (WhileStatement) rule;
                Expression whlExp = whlSt.getExpression();
                checkBoolExp(whlExp, "while or do-while", helper);
            }

            else if (rule instanceof IfStatementPart){
                IfStatementPart ifSt = (IfStatementPart) rule;
                Expression ifExp = ifSt.getExpression();
                checkBoolExp(ifExp, "if", helper);
            }

            else if (rule instanceof MethodCallStatement){
                MethodCallStatement ms = (MethodCallStatement) rule;

                PostfixExpression pexp = ms.getPostfixExpression();
                Type var = helper.setExpression(pexp).confirmType(pexp);
                
                if (ms.getExpression() != null){
                    // if we get in this section it is not method call - it is
                    // initialization of variable

                    // var = expr;
                    
                    Expression exp = ms.getExpression();

                    checkTypeCompatible(exp, var, helper, ms);
                }
            }

            else if (rule instanceof VariableDeclarator){
                VariableDeclarator vd = (VariableDeclarator) rule;
                Expression exp = vd.getExpression();
				String name = vd.getSimpleName();
				if (exp != null) {
					checkVarInit(name, exp, helper);
				} else {
					checkVarDeclar(name);
				}

            }

            else if (rule instanceof PrimaryFromIdentifier) {
                PrimaryFromIdentifier pfi = (PrimaryFromIdentifier) rule;
                checkVarDeclar(pfi.getSimpleName());

            }
        } catch (Exception ex){
            problems.add(ex);
        }
    }

    @Override
    public void onVisitEnd(Rule rule, boolean isAbstractRule) {
        
    }

    public List<Exception> getProblemsStack() {
        @SuppressWarnings("unchecked")
		ArrayList<Exception> clone = (ArrayList<Exception>) problems.clone();
        Collections.reverse(clone);
        return clone;
    }

    private void checkBoolExp(Expression exp, String statementName,
            TypeCheckingHelper helper) throws TypeCheckException {

        Type type = helper.confirmType(exp);
        assertEquals(BOOL, type, "Expression in " + statementName + " must be"
                + " boolean type (" + exp + ")");
    }

    private void checkVarInit(String id, Expression exp,
            TypeCheckingHelper helper) throws TypeCheckException {

        Type varType = checkVarDeclar(id);

        checkTypeCompatible(exp, varType, helper, exp);
    }

    private Type checkVarDeclar(String id) throws TypeCheckException {
        Record<?> var = symbolTable.lookup(new VarRecord(id));
        if (var == null){
        	var = symbolTable.getClass(id);
        }
        
        assertNotNull(var, "Variabl " + id + " not declared. ");
        return var.getType();
    }

    private void checkTypeCompatible(Expression exp, Type varType,
            TypeCheckingHelper helper, Object all) throws TypeCheckException {

        Type type = helper.confirmType(exp);
        assertNotNull(type, "assigning type of expression can't be empty");
        assertEquals(type, varType, "You can't to assign different types: "
                + varType + " <=> (" + type + ") " + exp + " (" + varType
                + " != " + all + ")");
    }

    private void compare(Type declarRetType, Expression exp,
            TypeCheckingHelper helper) throws TypeCheckException {
        // The different with checkTypeCompatible method is that this method
        // can take null type. So it's clean comparison of types (null = null).

        if (declarRetType != null && exp == null){
            problems.add(new CompilerException("illegal return statement, " +
            		"expect return value = " + declarRetType +" not (" + exp + ")"));
            return;
        }
        if (declarRetType == null && exp != null){
            problems.add(new CompilerException("illegal return statement, " +
            		"unexpect return value (" + exp + ")"));
            return;
        }
        if (declarRetType == null && exp == null){
        	// If both statement is null they are equal.
        	return; 
        }

        Type ct = helper.confirmType(exp);
        assertEquals(declarRetType, ct, "Wrong return type '" + ct + "' for "
                + "declared type '" + declarRetType + "'");
    }


    private void lookForReturnStatement(List<ReturnStatement> list, Statement st){
        if (st instanceof ReturnStatement){
            list.add((ReturnStatement) st);
        } else if (st instanceof Block){
            for (Statement rs : ((Block) st).getStatements()) {
                lookForReturnStatement(list, rs);
            }
        } else if (st instanceof IfElseStatement){
            IfElseStatement ifStmt = (IfElseStatement) st;
            lookForReturnStatement(list, ifStmt.getStatement());
            ElseStatementPart elseStatementPart = ifStmt.getElseStatementPart();
            if (elseStatementPart != null) {
                lookForReturnStatement(list, elseStatementPart.getStatement());
            }
        } else if (st instanceof ForStatement){
            lookForReturnStatement(list, ((ForStatement) st).getStatement());
        } else if (st instanceof WhileStatement){
            lookForReturnStatement(list, ((WhileStatement) st).getStatement());
        } else if (st instanceof DoWhileStatement){
            lookForReturnStatement(list, ((DoWhileStatement) st).getStatement());
        }

    }
}
